Skip to content

Initial LSP#51

Merged
ironm00n merged 21 commits intosandboxfrom
feat/lsp-init
Apr 8, 2026
Merged

Initial LSP#51
ironm00n merged 21 commits intosandboxfrom
feat/lsp-init

Conversation

@ironm00n
Copy link
Copy Markdown
Collaborator

@ironm00n ironm00n commented Apr 5, 2026

fixes: #40
fixes: #17
fixes: #18

ironm00n and others added 19 commits February 7, 2026 15:15
Co-authored-by: Zack Eisbach <zack.eisbach@gmail.com>
…for vscode extension with default configurations.
…to not conflict with the webExtension configuration.
Co-authored-by: Jacob Lefkowitz <jacoblefk@gmail.com>
Co-authored-by: ironmoon <me@ironmoon.dev>
Co-authored-by: ironmoon <me@ironmoon.dev>
Co-authored-by: ironmoon <me@ironmoon.dev>
With the goal of being able to support both file and in-memory cache.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ironm00n
Copy link
Copy Markdown
Collaborator Author

ironm00n commented Apr 5, 2026

@zackbach

@ironm00n ironm00n changed the title ????; DON'T UPSTREAM Initial LSP Apr 6, 2026
@ironm00n ironm00n requested review from Copilot and zackbach April 6, 2026 02:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces initial Language Server Protocol (LSP) scaffolding for Pyret by adding a VS Code node extension entrypoint that starts a language client, plus a TypeScript LSP server that forwards textDocument/definition (jump-to-def) requests to the existing Pyret compiler server, backed by a new in-memory compilation cache.

Changes:

  • Add a Node-targeted VS Code extension entry (src/extension.ts) and webpack build config alongside the existing web extension.
  • Add a new lsp/ TypeScript server project (including a temporary node server implementation) and wire it into the VS Code extension via vscode-languageclient.
  • Refactor the Pyret compiler server protocol to support “info” requests and add an in-memory cache manager + LSP-side “jump-to-def” implementation.

Reviewed changes

Copilot reviewed 41 out of 48 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
vscode/webpack.config.js Adds a Node-target webpack build for the desktop extension entrypoint.
vscode/src/webExtension.ts Minor formatting change; web extension activation remains.
vscode/src/extension.ts New Node extension entrypoint that starts a language client to the TS LSP server.
vscode/README.dev.md Updates dev instructions for testing web vs local desktop extension.
vscode/package.json Adds main entrypoint + vscode-languageclient dependency and a new trace setting.
vscode/package-lock.json Locks new dependency graph for vscode-languageclient.
vscode/.vscode/launch.json Adds a debug launch configuration for running the extension.
lsp/tsconfig.json TypeScript build config for the LSP server project.
lsp/src/server-shared.ts Shared LSP server setup (initialize, documents, listen).
lsp/src/server-node.ts Node LSP server entrypoint using vscode-languageserver/node.
lsp/src/server-node-tmp.ts Temporary node server implementing jump-to-def via the Pyret compiler server over WS+unix.
lsp/src/server-browser.ts Browser LSP server entrypoint using vscode-languageserver/browser.
lsp/README.md Documents current build/debug hoops for hacking on the LSP.
lsp/package.json Declares LSP server dependencies (vscode-languageserver, ws, etc.).
lsp/package-lock.json Locks LSP server dependency graph.
lsp/.gitignore Ignores node_modules and build output for the lsp/ package.
lang/tsconfig.json Adds a strict editor-only TS config for checking JS via checkJs.
lang/tests/type-check/main.arr Updates AST constructor calls to include locs.
lang/tests/pyret/tests/test-letrec.arr Adds use context empty-context to test file.
lang/tests/pyret/tests/test-compile-lib.arr Updates AST constructor calls to include locs.
lang/src/types.d.ts Adds TS declaration file to improve editor IntelliSense for runtime/ABI.
lang/src/js/trove/source-map-lib.js Wraps module object literal in parentheses; adds module typing hint.
lang/src/arr/trove/ast.arr Changes Name variants (s-global, s-type-global, etc.) to include a Loc; updates visitors.
lang/src/arr/compiler/type-structs.arr Adjusts pattern matches / constructors for updated Name shapes.
lang/src/arr/compiler/type-defaults.arr Updates globals/type globals to pass locs when constructing names.
lang/src/arr/compiler/type-check.arr Updates comparisons/pattern matches for new Name shapes.
lang/src/arr/compiler/server.js Refactors server protocol handler to accept {command, options} and adds a safer run queue.
lang/src/arr/compiler/server.arr Adds in-memory cache manager usage and an info command path for LSP requests.
lang/src/arr/compiler/resolve-scope.arr Threads locs through make-atom/global name creation and pattern matches updated.
lang/src/arr/compiler/lsp.arr New Pyret-side LSP helpers (currently jump-to-def via cached AST/env).
lang/src/arr/compiler/desugar.arr Updates global name construction to include loc.
lang/src/arr/compiler/desugar-post-tc.arr Updates global name construction to include loc.
lang/src/arr/compiler/compile-structs.arr Adds lsp + cache-manager fields and placeholder to-info-json / print-static-info.
lang/src/arr/compiler/compile-lib.arr Stores surface AST + named-result into the cache manager during compilation.
lang/src/arr/compiler/cli-module-loader.arr Introduces cache-manager abstraction, adds in-memory cache, and threads cache-manager through module-finding/caching.
lang/src/arr/compiler/ast-util.arr Updates name constructors and adds helpers for locating a name by srcloc and by cursor position.
lang/src/arr/compiler/ast-anf.arr Updates make-atom calls for new loc parameter.
lang/src/arr/compiler/anf.arr Updates make-atom calls for new loc parameter.
lang/src/arr/compiler/anf-loop-compiler.arr Updates pattern matches and atom creation for new Name shapes.
lang/package.json Adds @types/ws to dev dependencies.
lang/package-lock.json Locks @types/ws addition and related metadata changes.
docs/src/trove/ast.js.rkt Updates AST docs to reflect l member added to name variants.
code.pyret.org/src/web/js/output-ui.js Updates comment about which AST nodes have l fields.
code.pyret.org/src/web/js/ide.js Removes unused React import.
code.pyret.org/package-lock.json Dependency metadata changes (peer flags / bundle reshuffles).
.gitignore Ignores root /.vscode/ directory.
.biome.json Adds Biome formatter config.
Files not reviewed (4)
  • code.pyret.org/package-lock.json: Language not supported
  • lang/package-lock.json: Language not supported
  • lsp/package-lock.json: Language not supported
  • vscode/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)

lang/src/arr/compiler/cli-module-loader.arr:512

  • default-start-context / default-test-context set cache-manager to CS.default-compile-options.cache-manager, but that default cache-manager only provides set/get-surface-ast and set/get-named-result (no cached-available, get-cached, etc.). Since module-finder now calls ctxt.cache-manager.cached-available(...) and friends, these contexts will throw at runtime (e.g. scripts/tests using CLI.default-start-context). Use make-file-cache() (or another full CacheManger implementation) for these defaults.
default-start-context = {
  cache-manager: CS.default-compile-options.cache-manager,
  current-load-path: Filesystem.resolve("./"),
  cache-base-dir: Filesystem.resolve("./compiled"),
  compiled-read-only-dirs: empty,
  url-file-mode: CS.all-remote
}

default-test-context = {
  cache-manager: CS.default-compile-options.cache-manager,
  current-load-path: Filesystem.resolve("./"),
  cache-base-dir: Filesystem.resolve("./tests/compiled"),
  compiled-read-only-dirs: empty,
  url-file-mode: CS.all-remote

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread vscode/src/extension.ts
Comment thread lsp/src/server-node-tmp.ts
Comment on lines +52 to +58
const child = childProcess.fork(
compilerPath,
["-serve", "--port", portFile],
{
stdio: [0, 1, 2, "ipc"],
execArgv: ["-max-old-space-size=8192"],
},
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

child_process.fork execArgv uses Node/V8 flags, which are typically --max-old-space-size=... (double hyphen). Using -max-old-space-size=8192 is not a recognized Node flag and may be ignored, defeating the intended memory limit increase.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +12
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "npm: compile"
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug configuration’s outFiles points at ${workspaceFolder}/out/**/*.js, but this extension is built via webpack to dist/ (and the new node entrypoint outputs to dist/extension.js). With the current value, breakpoints/source-maps likely won’t bind. Update outFiles to match the emitted dist/**/*.js (or whatever the actual build output is).

Copilot uses AI. Check for mistakes.
Comment thread vscode/package.json
Comment on lines 18 to 26
"files": [
"dist/web/**/*",
"media/pyret-logo-128.png",
"language-configuration.json",
"syntaxes/pyret.tmLanguage.json"
],
"browser": "./dist/web/extension.js",
"main": "./dist/extension.js",
"contributes": {
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main now points to ./dist/extension.js, but the package’s files allowlist currently only includes dist/web/**/* (not dist/extension.js). This will make the node entrypoint missing from packaged installs. Add the node bundle (and its sourcemaps, if needed) to the files list or adjust the output location.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zackbach is this legit?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I have little idea... might be a Joe question??

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more important thing is figuring out how to package and distribute for the web...

Comment thread vscode/package.json
Comment on lines 120 to +129
],
"description": "Controls how `url-file()` imports are resolved"
}
},
"pyret.trace.server": {
"type": "string",
"enum": ["off", "messages", "verbose"],
"default": "verbose",
"scope": "resource",
"description": "Traces logs of the communication between VS Code and the language server."
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new setting pyret.trace.server is contributed here, but there’s no code wiring it up (e.g., calling client.setTrace(...) or configuring trace level based on this setting). As-is, changing the setting will have no effect. Either plumb it into LanguageClient initialization or remove it until it’s supported.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ironm00n seems legit, maybe check this out?

Comment thread lang/src/arr/compiler/cli-module-loader.arr Outdated
Comment thread lang/src/arr/compiler/ast-util.arr
Comment thread lang/src/arr/compiler/cli-module-loader.arr Outdated
Copy link
Copy Markdown
Collaborator

@zackbach zackbach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great! Mostly documentation nits, and a few opportunities for cleaner code.

Comment thread lang/src/arr/compiler/ast-util.arr
Comment thread lang/src/arr/compiler/cli-module-loader.arr
Comment thread lang/src/arr/compiler/cli-module-loader.arr
Comment thread lang/src/arr/compiler/cli-module-loader.arr
Comment thread lang/src/arr/compiler/cli-module-loader.arr
Comment thread lang/src/arr/trove/ast.arr
Comment thread lang/tests/pyret/tests/test-letrec.arr
}

// NOTE(lsp): To add a new LSP feature:
// 1. Add a send*Request function below (following this pattern)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems likely that we can factor out some of the boilerplate here. I should be able to do this for hover.

Comment thread vscode/package.json
Comment on lines 18 to 26
"files": [
"dist/web/**/*",
"media/pyret-logo-128.png",
"language-configuration.json",
"syntaxes/pyret.tmLanguage.json"
],
"browser": "./dist/web/extension.js",
"main": "./dist/extension.js",
"contributes": {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more important thing is figuring out how to package and distribute for the web...

Comment thread vscode/package.json
Comment on lines 120 to +129
],
"description": "Controls how `url-file()` imports are resolved"
}
},
"pyret.trace.server": {
"type": "string",
"enum": ["off", "messages", "verbose"],
"default": "verbose",
"scope": "resource",
"description": "Traces logs of the communication between VS Code and the language server."
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ironm00n seems legit, maybe check this out?

@ironm00n ironm00n force-pushed the feat/lsp-init branch 3 times, most recently from 8ccb27c to 0bb800c Compare April 8, 2026 22:36
Copy link
Copy Markdown
Collaborator Author

@ironm00n ironm00n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultra-Review: PR #51 — Initial LSP

+2197 / -575 across 46 files. Core changes: (1) add Loc to Name AST variants, (2) Pyret-side LSP query infrastructure, (3) TS LSP server + VS Code node extension.


CRITICAL (4)

C1. server.js:145 — Unguarded JSON.parse crashes the server on malformed WebSocket input

No try/catch around JSON.parse(message). A single bad message from any client takes down the entire Pyret compilation server.

C2. server.js:141message is a Buffer in ws 8.x, not string

The @type {string} JSDoc is wrong. ws 8.x delivers Buffer by default. Works by accident (Node's JSON.parse accepts Buffer), but any typeof check will fail.

C3. server-node-tmp.ts:57execArgv: ["-max-old-space-size=8192"] needs double dash

Node V8 flags require --max-old-space-size=8192. Single-dash is silently ignored, so the 8GB heap limit is not applied.

C4. types.d.ts:13-19pyret_pos missing endCol field

The runtime reads p.endCol (ffi.js:51) but the type has no endCol. Code using this type will produce undefined for endCol, causing NaN in source locations.


MAJOR (14)

M1. server.js:119-186 — No 'error' handler on WebSocket connections

In Node, an unhandled 'error' event crashes the process. A client disconnect (TCP reset) will emit an error event, taking down the server.

M2. server.js:58-87 — Queued items hold references to closed WebSocket connections

No connection.readyState check before connection.send(). If a client disconnects while its request is queued, send() throws.

M3. server-node-tmp.ts:150-206 — WebSocket connections leak (never closed)

sendJumpToDefRequest opens a new WebSocket per request but never calls client.close() after resolving/rejecting.

M4. server-node-tmp.ts:150-206 — No timeout on WebSocket requests

If the Pyret server hangs, the promise never settles and the LSP client freezes indefinitely.

M5. server-node-tmp.ts:176 — Unguarded JSON.parse in WebSocket message handler

Same issue as C1, but on the LSP server side. Malformed data from the Pyret server crashes the LSP process.

M6. server-node-tmp.ts:112-119 — Stale socket file causes silent failure

If a previous server crashed without cleanup, the socket file exists but no server is listening. The code skips startPyretServer and all subsequent requests fail. No liveness check.

M7. server.arr:140-145ask block with no otherwise fallback

Only "jump-to-def" is handled. An unknown query string throws an exception instead of returning a clean error. Same issue at lines 160-178 for serialization.

M8. query.arr — All failure paths return E.left([list:]) with no diagnostic info

Every none branch returns an empty error list. The caller logs torepr(errors) which is always "[list:]". Makes debugging impossible.

M9. compile-lib.arr:431 — Query path skips canonicalize-provides

When options.query is true, provides aren't canonicalized (unlike the normal path at line 439). Downstream code expecting module-uri(...) NameOrigins may break.

M10. extension.ts:22-24serverModule path resolves outside the extension directory

context.asAbsolutePath(path.join("..", "lsp", "out", "server-node-tmp.js")) escapes the extension root. Works in dev, breaks in any packaged/installed extension.

M11. vscode/package.json:18-23files excludes dist/extension.js

The files allowlist only has dist/web/**/*. The node entry point won't be in the packaged extension.

M12. types.d.ts:189PFunction.name typed as number, should be string

Runtime sets this.name = name || "anonymous". Constructor param is string but the property is declared number.

M13. types.d.ts:334makeOpqaue typo (should be makeOpaque)

Will never match the actual runtime export. Also missing the equals parameter.

M14. types.d.ts:265-268runThunk generic signature doesn't match runtime

Callback receives SuccessResult | FailureResult, not T. The type parameter U is declared but unused.


MINOR (14)

  • m1. server-node-tmp.ts:41-86startPyretServer promise can settle twice. No guard flag on "error" / "exit" handlers.
  • m2. server-node-tmp.ts:66child.unref() + disconnect() means exit handler never fires. pyretServerProcess stays non-null forever.
  • m3. cli-module-loader.arr:48-59CacheManager type incomplete. Missing get-cached-worklist, set-cached-worklist, invalidate. File-based cache doesn't implement these.
  • m4. compile-structs.arr:2986-2991 — Default cache-manager stub only has 4 methods. Any code hitting other methods crashes.
  • m5. server.js:92-98 — No error handler on HTTP server listen. process.send({type: 'success'}) fires before the server is actually listening.
  • m6. server.js:97 — Double timestamp in log messages. makeLogger already prepends new Date().
  • m7. vscode/.vscode/launch.json:10outFiles points to out/ but webpack outputs to dist/. Breakpoints won't bind.
  • m8. vscode/package.json:126pyret.trace.server defaults to "verbose". Will flood the output channel for all users.
  • m9. lsp/package.json:14@types/ws in dependencies instead of devDependencies.
  • m10. lsp/package.json:7main: "index.js" points to nonexistent file.
  • m11. ast-util.arr:1551-1601find-name-key-by-srcloc missing s-type-global / s-module-global handlers. Jump-to-def silently fails for type names and module names.
  • m12. extension.ts:58client.start() not awaited/caught. Unhandled rejection if server fails to start.
  • m13. types.d.ts:347-364check* functions accept unknown, not the specific JS types declared. These are runtime assertion functions on Pyret values.
  • m14. types.d.ts:180POpaque missing val and equals fields.

NITS (6)

  • ast.arr — Multiple # ZACK: comments should be cleaned up or formalized.
  • server.arr:43-46 — Commented-out print statements.
  • server-node-tmp.ts — File named -tmp but is the actual entry point.
  • server-node.ts — Dead code (nothing references it).
  • lsp/package.json:5"license": "UNLICENSED" on an open-source repo.
  • server.js:169 — Stray semicolon after case block brace.

Pre-existing bugs found (not blocking, but worth tracking)

  • ast-util.arr:1106t-ref(ann-to-typ(ann), false) missing l argument (needs 3 args).
  • ast-util.arr:271s-method reconstructed with wrong arity (drops name and blocky).

Name-carries-Loc consistency: PASS

All pattern matches on s-global, s-module-global, s-type-global, s-atom across resolve-scope, type-check, type-structs, type-defaults, desugar, desugar-post-tc, anf, anf-loop-compiler, and ast-anf have been updated correctly. Name._equals compares by .key() only (excludes loc), so == comparisons with dummy-loc are safe.


Top 5 things to fix before merge

  1. C1+M1 — Add error handling to server.js (try/catch JSON.parse, add ws error handlers). A single bad message shouldn't kill the compilation server.
  2. C3 — Double-dash on --max-old-space-size=8192.
  3. M3+M4 — Close WebSocket connections after use and add a timeout in sendJumpToDefRequest.
  4. M8 — Add diagnostic strings to query.arr failure paths so debugging is possible.
  5. M10+M11 — Fix extension packaging (files allowlist, serverModule path) before it bites someone.

Generated by Claude Code (claude-opus-4-6)

@ironm00n ironm00n merged commit bef75b0 into sandbox Apr 8, 2026
@ironm00n ironm00n deleted the feat/lsp-init branch April 8, 2026 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LSP: Initial In-Memory Cache Setup + Refactoring LSP: support textDocument/definition Minimally extend Pyret's server with LSP scaffolding

4 participants